{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "initial_id",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:39.580119Z",
     "start_time": "2025-01-21T14:09:39.573580Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "1dad802b6500ab83",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.656263Z",
     "start_time": "2025-01-21T14:09:39.699901Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(PosixPath('competitions/cifar-10/train/1.png'), 'frog'),\n",
      " (PosixPath('competitions/cifar-10/train/2.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/3.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/4.png'), 'deer'),\n",
      " (PosixPath('competitions/cifar-10/train/5.png'), 'automobile')]\n",
      "[(PosixPath('competitions/cifar-10/test/1.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/2.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/3.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/4.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/5.png'), 'cat')]\n",
      "50000 300000\n"
     ]
    }
   ],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "# 目的：通过读取csv文件，得到图片的类别，并将图片路径和类别保存到DataFrame中。\n",
    "\n",
    "DATA_DIR1 = Path(\"./\")\n",
    "DATA_DIR2=Path(\"./competitions/cifar-10/\")\n",
    "\n",
    "train_labels_file = DATA_DIR1 / \"trainLabels.csv\"\n",
    "test_csv_file = DATA_DIR1 / \"sampleSubmission.csv\"  # 测试集模板csv文件\n",
    "train_folder = DATA_DIR2 / \"train/\"\n",
    "test_folder = DATA_DIR2 / \"test/\"\n",
    "\n",
    "# 所有的类别\n",
    "class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']\n",
    "\n",
    "\n",
    "# filepath:csv文件路径，folder:图片所在文件夹\n",
    "def parse_csv_file(filepath, folder):\n",
    "    result = []\n",
    "    # 读取所有行\n",
    "    # with 语句：用于管理文件的上下文。在 with 块结束时，文件会自动关闭，无需手动调用 f.close()。\n",
    "    with open(filepath, 'r') as f:\n",
    "        # f.readlines()：读取文件的所有行，返回一个列表，每个元素是一行内容。\n",
    "        # 第一行不需要，因为第一行是标题\n",
    "        lines = f.readlines()[1:]\n",
    "    for line in lines:\n",
    "        # strip('\\n')：去除行末的换行符（\\n）。\n",
    "        # split(',')：按','分割字符串，返回一个列表。\n",
    "        image_id, label_str = line.strip('\\n').split(',')\n",
    "        # 得到图片的路径\n",
    "        image_full_path = folder / f\"{image_id}.png\"\n",
    "        # 得到对应图片的路径和分类\n",
    "        result.append((image_full_path, label_str))\n",
    "    return result\n",
    "\n",
    "\n",
    "# 得到训练集和测试集的图片路径和分类\n",
    "train_labels_info = parse_csv_file(train_labels_file, train_folder)\n",
    "test_csv_info = parse_csv_file(test_csv_file, test_folder)\n",
    "\n",
    "#打印\n",
    "import pprint\n",
    "\n",
    "pprint.pprint(train_labels_info[0:5])\n",
    "pprint.pprint(test_csv_info[0:5])\n",
    "print(len(train_labels_info), len(test_csv_info))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8cd5063c39c678cf",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.732204Z",
     "start_time": "2025-01-21T14:09:41.658275Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                            filepath       class\n",
      "0  competitions/cifar-10/train/1.png        frog\n",
      "1  competitions/cifar-10/train/2.png       truck\n",
      "2  competitions/cifar-10/train/3.png       truck\n",
      "3  competitions/cifar-10/train/4.png        deer\n",
      "4  competitions/cifar-10/train/5.png  automobile\n",
      "                                filepath       class\n",
      "0  competitions/cifar-10/train/45001.png       horse\n",
      "1  competitions/cifar-10/train/45002.png  automobile\n",
      "2  competitions/cifar-10/train/45003.png        deer\n",
      "3  competitions/cifar-10/train/45004.png  automobile\n",
      "4  competitions/cifar-10/train/45005.png    airplane\n",
      "                           filepath class\n",
      "0  competitions/cifar-10/test/1.png   cat\n",
      "1  competitions/cifar-10/test/2.png   cat\n",
      "2  competitions/cifar-10/test/3.png   cat\n",
      "3  competitions/cifar-10/test/4.png   cat\n",
      "4  competitions/cifar-10/test/5.png   cat\n"
     ]
    }
   ],
   "source": [
    "# 取前45000张图片作为训练集\n",
    "train_df = pd.DataFrame(train_labels_info[0:45000])\n",
    "# 取后5000张图片作为验证集\n",
    "valid_df = pd.DataFrame(train_labels_info[45000:])\n",
    "# 测试集\n",
    "test_df = pd.DataFrame(test_csv_info)\n",
    "\n",
    "# 为 Pandas DataFrame 的列重新命名\n",
    "train_df.columns = ['filepath', 'class']\n",
    "valid_df.columns = ['filepath', 'class']\n",
    "test_df.columns = ['filepath', 'class']\n",
    "\n",
    "print(train_df.head())\n",
    "print(valid_df.head())\n",
    "print(test_df.head())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "af9444393c2debca",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.740688Z",
     "start_time": "2025-01-21T14:09:41.732204Z"
    }
   },
   "outputs": [],
   "source": [
    "from PIL import Image\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torchvision import transforms\n",
    "\n",
    "\n",
    "class Cifar10Dataset(Dataset):\n",
    "    df_map = {\n",
    "        \"train\": train_df,\n",
    "        \"eval\": valid_df,\n",
    "        \"test\": test_df\n",
    "    }\n",
    "    # enumerate(class_names)：遍历 class_names，返回索引和类别名称的元组\n",
    "    # 将类别名称映射为索引，通常用于将标签转换为模型可以处理的数值。\n",
    "    label_to_idx = {\n",
    "        label: idx for idx, label in enumerate(class_names)\n",
    "    }\n",
    "    # 将索引映射为类别名称，通常用于将模型的输出（索引）转换回可读的类别名称\n",
    "    idx_to_label = {\n",
    "        idx: label for idx, label in enumerate(class_names)\n",
    "    }\n",
    "\n",
    "    def __init__(self, mode, transform=None):\n",
    "        # 获取对应模式的df，不同字符串对应不同模式\n",
    "        self.df = self.df_map.get(mode, None)\n",
    "        if self.df is None:\n",
    "            raise ValueError(f\"Invalid mode: {mode}\")\n",
    "        self.transform = transform\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        # 获取图片路径和标签\n",
    "        img_path, label = self.df.iloc[index]\n",
    "        # 打开一张图片并将其转换为 RGB 格式\n",
    "        img = Image.open(img_path).convert('RGB')  # 确保图片具有三个通道\n",
    "        # 应用数据增强\n",
    "        img = self.transform(img)\n",
    "        # label 转换为 idx\n",
    "        label = self.label_to_idx[label]\n",
    "        return img, label\n",
    "\n",
    "    def __len__(self):\n",
    "        # 返回df的行数,样本数\n",
    "        return self.df.shape[0]\n",
    "\n",
    "\n",
    "IMAGE_SIZE = 32\n",
    "mean, std = [0.4914, 0.4822, 0.4465], [0.247, 0.243, 0.261]\n",
    "\n",
    "#数据增强\n",
    "# ToTensor还将图像的维度从[height, width, channels]转换为[channels, height, width]。\n",
    "transforms_train = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),  # 缩放\n",
    "    transforms.RandomRotation(40),  # 随机旋转\n",
    "    transforms.RandomHorizontalFlip(),  # 随机水平翻转\n",
    "    transforms.ToTensor(),  # 转换为Tensor\n",
    "    transforms.Normalize(mean, std)  # 标准化\n",
    "])\n",
    "\n",
    "transforms_eval = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize(mean, std)\n",
    "])\n",
    "\n",
    "train_ds = Cifar10Dataset(mode=\"train\", transform=transforms_train)\n",
    "eval_ds = Cifar10Dataset(mode=\"eval\", transform=transforms_eval)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d17ef4a56f0b6ae1",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.748174Z",
     "start_time": "2025-01-21T14:09:41.742701Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([3, 32, 32])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_ds[0][0].shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "161282a4e612afcd",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.753134Z",
     "start_time": "2025-01-21T14:09:41.749187Z"
    }
   },
   "outputs": [],
   "source": [
    "batch_size = 64\n",
    "\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size,\n",
    "                          shuffle=True)\n",
    "eval_loader = DataLoader(eval_ds, batch_size=batch_size,\n",
    "                         shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20b5b31416450980",
   "metadata": {},
   "source": [
    "## 模型定义"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b73daeeb83b4b527",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.760182Z",
     "start_time": "2025-01-21T14:09:41.754138Z"
    }
   },
   "outputs": [],
   "source": [
    "class Resdiual(nn.Module):\n",
    "    \"\"\"\n",
    "    浅层的残差块，无bottleneck（性能限制\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels,\n",
    "                 use_1x1conv=False, stride=1):\n",
    "        \"\"\"\n",
    "        残差块\n",
    "        params filters: 过滤器数目，决定输出通道\n",
    "        params use_1x1conv: 是否使用 1x1 卷积，此时 stride=2，进行降采样\n",
    "        params strides: 步长，默认为1，当降采样的时候设置为2\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        # [1+(n+2-3)]//2=n//2\n",
    "        # 即该方法可能会降采样\n",
    "        self.conv1 = nn.Conv2d(\n",
    "            in_channels=input_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=stride,\n",
    "            padding=1\n",
    "        )\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        self.conv2 = nn.Conv2d(\n",
    "            in_channels=output_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=1,\n",
    "            padding=1\n",
    "        )\n",
    "        if use_1x1conv:\n",
    "            # skip connection 的 1x1 卷积，用于改变通道数和降采样，使得最终可以做残差连接\n",
    "            # [1+n-1]//s=n//s\n",
    "            self.conv_sc = nn.Conv2d(\n",
    "                in_channels=input_channels,\n",
    "                out_channels=output_channels,\n",
    "                kernel_size=1,\n",
    "                stride=stride\n",
    "            )\n",
    "        else:\n",
    "            self.conv_sc = None\n",
    "\n",
    "        self.bn1 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "        self.bn2 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        flow = F.relu(self.bn1(self.conv1(inputs)))  # 卷积->BN->ReLU\n",
    "        flow = self.bn2(self.conv2(flow))  # 卷积->BN\n",
    "        if self.conv_sc:\n",
    "            inputs = self.conv_sc(inputs)  # 1x1卷积，改变通道数和降采样\n",
    "        # 残差连接->ReLU，必须保证flow和inputs的shape相同\n",
    "        return F.relu(flow + inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "fc16467b5391fd55",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.766476Z",
     "start_time": "2025-01-21T14:09:41.761185Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResdiualBlock(nn.Module):\n",
    "    \"\"\"\n",
    "    若干个 Resdiual 模块堆叠在一起，\n",
    "    通常在第一个模块给 skip connection 使用 1x1conv with stride=2\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels, num, is_first=False):\n",
    "        \"\"\"\n",
    "        params filters: 过滤器数目\n",
    "        params num: 堆叠几个 Resdiual 模块\n",
    "        params is_first: 是不是第一个block。 最上面一层 Resdiual 的 stride=1,is_first=False,图像尺寸减半，False图像尺寸不变\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential()  # 用于存放 Resdiual 模块\n",
    "        # append() 等价于 add_module()\n",
    "        self.model.append(Resdiual(\n",
    "            input_channels=input_channels,\n",
    "            output_channels=output_channels,\n",
    "            use_1x1conv=not is_first,\n",
    "            stride=1 if is_first else 2\n",
    "        ))  # 第一个 Resdiual 模块，负责通道翻倍,图像的尺寸减半\n",
    "        for _ in range(1, num):\n",
    "            # 堆叠 num 个 Resdiual 模块\n",
    "            self.model.append(Resdiual(\n",
    "                input_channels=output_channels,\n",
    "                output_channels=output_channels,\n",
    "                use_1x1conv=False,\n",
    "                stride=1\n",
    "            ))\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "8128761d5d6c0e1d",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.773688Z",
     "start_time": "2025-01-21T14:09:41.767479Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResNetCifar10(nn.Module):\n",
    "    def __init__(self, n=3, num_classes=10):\n",
    "        \"\"\"\n",
    "        params units: 预测类别的数目\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential(\n",
    "            # conv1\n",
    "            nn.Conv2d(in_channels=3, out_channels=16,\n",
    "                      kernel_size=3, stride=1),\n",
    "            nn.BatchNorm2d(16, momentum=0.9, eps=1e-5),\n",
    "            nn.ReLU(),\n",
    "            # conv2_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=16,\n",
    "                          num=2 * n, is_first=True),\n",
    "            # conv3_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=32,\n",
    "                          num=2 * n),\n",
    "            # conv4_x\n",
    "            ResdiualBlock(input_channels=32, output_channels=64,\n",
    "                          num=2 * n),\n",
    "            # 全局平均池化层\n",
    "            #无论输入图片大小，输出都是1x1，把width和height压缩为1\n",
    "            nn.AdaptiveAvgPool2d((1, 1)),\n",
    "            # 全连接层\n",
    "            nn.Flatten(),  # 64*1*1 -> 64\n",
    "            # 输出层\n",
    "            nn.Linear(in_features=64, out_features=num_classes)\n",
    "        )\n",
    "        self.init_weights()\n",
    "\n",
    "    def init_weights(self):\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
    "                # Kaiming 初始化（也称为 He 初始化）是为深度神经网络设计的一种初始化方法，特别适用于 ReLU 激活函数。\n",
    "                nn.init.kaiming_uniform_(m.weight)\n",
    "                if m.bias is not None:\n",
    "                    nn.init.zeros_(m.bias)\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bb519b4756d62090",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.777720Z",
     "start_time": "2025-01-21T14:09:41.774691Z"
    }
   },
   "outputs": [],
   "source": [
    "# for key, value in ResNetCifar10(len(class_names)).named_parameters():\n",
    "#     print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "b294d25e7f6287d0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.825513Z",
     "start_time": "2025-01-21T14:09:41.779726Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 1929546\n"
     ]
    }
   ],
   "source": [
    "total_params = sum(p.numel() for p in ResNetCifar10(len(class_names)).parameters() if p.requires_grad)\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76ca49d0dca74376",
   "metadata": {},
   "source": [
    "## 模型训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "632d31ce33563b0c",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.909287Z",
     "start_time": "2025-01-21T14:09:41.826518Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "\n",
    "@torch.no_grad()  # 装饰器，禁止梯度计算\n",
    "def evaluate(model, data_loader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in data_loader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "\n",
    "        # 前向传播\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)  # 验证集损失\n",
    "        # tensor.item() 获取tensor的数值，loss是只有一个元素的tensor\n",
    "        loss_list.append(loss.item())\n",
    "\n",
    "        # 预测\n",
    "        preds = logits.argmax(axis=-1)  # 预测类别\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())  # tensor转numpy，再转list\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "\n",
    "    acc = accuracy_score(label_list, pred_list)  # 计算准确率\n",
    "    return np.mean(loss_list), acc  # # 返回验证集平均损失和准确率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "28c2ba49f4ff546",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.915505Z",
     "start_time": "2025-01-21T14:09:41.910290Z"
    }
   },
   "outputs": [],
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=500, save_best_only=True):\n",
    "        self.save_dir = save_dir  # 保存路径\n",
    "        self.save_step = save_step  # 保存步数\n",
    "        self.save_best_only = save_best_only  # 是否只保存最好的模型\n",
    "        self.best_metric = -1  # 最好的指标，指标不可能为负数，所以初始化为-1\n",
    "        # 创建保存路径\n",
    "        if not os.path.exists(self.save_dir):  # 如果不存在保存路径，则创建\n",
    "            os.makedirs(self.save_dir)\n",
    "\n",
    "    # 对象被调用时：当你将对象像函数一样调用时，Python 会自动调用 __call__ 方法。\n",
    "    # state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "    # metric 是指标，可以是验证集的准确率，也可以是其他指标\n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return  # 不是保存步数，则直接返回\n",
    "\n",
    "        if self.save_best_only:\n",
    "            assert metric is not None  # 必须传入metric\n",
    "            if metric >= self.best_metric:  # 如果当前指标大于最好的指标\n",
    "                # save checkpoint\n",
    "                # 保存最好的模型，覆盖之前的模型，不保存step，只保存state_dict，即模型参数，不保存优化器参数\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"08_resnet.ckpt\"))\n",
    "                self.best_metric = metric  # 更新最好的指标\n",
    "        else:\n",
    "            # 保存模型\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "            # 保存每个step的模型，不覆盖之前的模型，保存step，保存state_dict，即模型参数，不保存优化器参数\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "cd4d61586ad24eb5",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.921575Z",
     "start_time": "2025-01-21T14:09:41.916508Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        self.patience = patience  # 多少个step没有提升就停止训练\n",
    "        self.min_delta = min_delta  # 最小的提升幅度\n",
    "        self.best_metric = -1  # 记录的最好的指标\n",
    "        self.counter = 0  # 计数器，记录连续多少个step没有提升\n",
    "\n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:  # 如果指标提升了\n",
    "            self.best_metric = metric  # 更新最好的指标\n",
    "            self.counter = 0  # 计数器清零\n",
    "        else:\n",
    "            self.counter += 1  # 计数器加一\n",
    "\n",
    "    @property  # 使用@property装饰器，使得 对象.early_stop可以调用，不需要()\n",
    "    def early_stop(self):\n",
    "        # 如果计数器大于等于patience，则返回True，停止训练\n",
    "        return self.counter >= self.patience"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "725cf8a521379801",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.930997Z",
     "start_time": "2025-01-21T14:09:41.922578Z"
    }
   },
   "outputs": [],
   "source": [
    "def training(model,\n",
    "             train_loader,\n",
    "             val_loader,\n",
    "             epoch,\n",
    "             loss_fct,\n",
    "             optimizer,\n",
    "             save_ckpt_callback=None,\n",
    "             early_stop_callback=None,\n",
    "             eval_step=500,\n",
    "             ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "\n",
    "    global_step = 0  # 全局步数\n",
    "    model.train()  # 训练模式\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "\n",
    "                # 前向传播\n",
    "                logits = model(datas)\n",
    "                loss = loss_fct(logits, labels)  # 训练集损失\n",
    "                preds = logits.argmax(axis=-1)  # 预测类别\n",
    "\n",
    "                # 反向传播\n",
    "                optimizer.zero_grad()  # 梯度清零\n",
    "                loss.backward()  # 反向传播\n",
    "                optimizer.step()  # 优化器更新参数\n",
    "\n",
    "                # 计算准确率\n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())\n",
    "                loss = loss.cpu().item()\n",
    "\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss,\n",
    "                    \"acc\": acc,\n",
    "                    \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # 评估\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()  # 评估模式\n",
    "                    # 验证集损失和准确率\n",
    "                    val_loss, val_acc = evaluate(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss,\n",
    "                        \"acc\": val_acc,\n",
    "                        \"step\": global_step\n",
    "                    })\n",
    "                    model.train()  # 训练模式\n",
    "\n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        # model.state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), val_acc)\n",
    "                        # 保存最好的模型，覆盖之前的模型，保存step，保存state_dict,通过metric判断是否保存最好的模型\n",
    "\n",
    "                    # 3. 早停 early stopping\n",
    "                    if early_stop_callback is not None:\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        early_stop_callback(val_acc)\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict  # 早停，返回记录字典 record_dict\n",
    "\n",
    "                # 更新进度条和全局步数\n",
    "                pbar.update(1)  # 更新进度条\n",
    "                global_step += 1  # 全局步数加一\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "\n",
    "    return record_dict  # 训练结束，返回记录字典 record_dict\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "22bf1e82c9715088",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.979444Z",
     "start_time": "2025-01-21T14:09:41.931999Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "epoch = 100\n",
    "\n",
    "model = ResNetCifar10(len(class_names))  # 定义模型\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "\n",
    "# 2. 定义优化器 采用RMSprop\n",
    "optimizer = torch.optim.RMSprop(model.parameters(), lr=0.001, alpha=0.9, eps=1e-07)\n",
    "\n",
    "# 3.save model checkpoint\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(save_dir=\"checkpoints\", save_step=len(train_loader), save_best_only=True)\n",
    "\n",
    "# 4. early stopping\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "5335f62fe5e4977f",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.118493Z",
     "start_time": "2025-01-21T14:09:41.980446Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 22%|██▏       | 15488/70400 [19:03<1:07:34, 13.54it/s, epoch=21]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 22 / global_step 15488\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "model = model.to(device)  # 将模型移到GPU上\n",
    "\n",
    "# 训练过程\n",
    "record_dict = training(\n",
    "    model,\n",
    "    train_loader,\n",
    "    eval_loader,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "1a790e5f4635d5e8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.120498Z",
     "start_time": "2025-01-21T14:10:19.119497Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAz8AAAHFCAYAAADcw0cVAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAnlRJREFUeJzs3Xl4lNX5//H3zGSyJ4QQshACYV9kFQRRRFAWRakotm51oRargtWmLqV1w7bSb/WH2BaldW1VqrWitRaRiKKyCAiy72tYkpCF7NtkZn5/PJmBQJaZZJJMks/ruriSeeZZzuQwyXPPuc99TE6n04mIiIiIiEgbZ27pBoiIiIiIiDQHBT8iIiIiItIuKPgREREREZF2QcGPiIiIiIi0Cwp+RERERESkXVDwIyIiIiIi7YKCHxERERERaRcU/IiIiIiISLug4EdERERERNoFBT8iIiIiItIueBX8zJ8/n4suuoiIiAhiY2OZPn06e/furbbP+PHjMZlM1f7de++91fZJS0vjmmuuITQ0lNjYWB555BEqKysb/2pERERERERqEeDNzl999RWzZ8/moosuorKykl//+tdMnjyZXbt2ERYW5t5v1qxZPPPMM+7HoaGh7u/tdjvXXHMN8fHxrF27lvT0dO644w6sVivPPvusD16SiIiIiIjI+UxOp9PZ0IOzsrKIjY3lq6++Yty4cYAx8jNs2DAWLlxY4zGffvop1157LSdPniQuLg6AxYsX89hjj5GVlUVgYGBDmyMiIiIiIlIrr0Z+zpWfnw9AdHR0te3vvPMOb7/9NvHx8UybNo0nnnjCPfqzbt06Bg8e7A58AKZMmcJ9993Hzp07GT58+HnXKS8vp7y83P3Y4XCQm5tLp06dMJlMjXkJIiLiBafTSWFhIV26dMFs1rTRszkcDk6ePElERIT+NomINCNv/jY1OPhxOBw89NBDXHrppQwaNMi9/dZbb6V79+506dKFbdu28dhjj7F3716WLl0KQEZGRrXAB3A/zsjIqPFa8+fPZ968eQ1tqoiI+NixY8fo2rVrSzfDr5w8eZKkpKSWboaISLvlyd+mBgc/s2fPZseOHaxevbra9nvuucf9/eDBg0lISODKK6/k4MGD9OrVq0HXmjt3LikpKe7H+fn5dOvWjcOHDxMREeH1+Ww2G19++SUTJkzAarU2qE3SNNQ3/k3947+aq28KCwvp0aNHg373NrdFixbx3HPPkZGRwdChQ/nzn//MqFGjatzXZrMxf/58/v73v3PixAn69evH//3f/3HVVVd5fD3Xz+TYsWNERkZ63V6bzcaKFSuYPHmy3l9+Rn3j39Q//qu5+qagoICkpCSP/jY1KPiZM2cOn3zyCV9//XW90dXo0aMBOHDgAL169SI+Pp4NGzZU2yczMxOA+Pj4Gs8RFBREUFDQedujo6Mb/AcmNDSUTp066U3iZ9Q3/k3947+aq29c5/b3tK733nuPlJQUFi9ezOjRo1m4cCFTpkxh7969xMbGnrf/448/zttvv80rr7xC//79+eyzz7j++utZu3ZtjenYNXH9TCIjIxv1tykyMlLvLz+jvvFv6h//1dx948nfJq8Stp1OJ3PmzOHDDz/kiy++oEePHvUes2XLFgASEhIAGDNmDNu3b+fUqVPufVJTU4mMjGTgwIHeNEdERKRGCxYsYNasWcycOZOBAweyePFiQkNDef3112vc/6233uLXv/41U6dOpWfPntx3331MnTqV//f//l8zt1xERJqSV8HP7Nmzefvtt1myZAkRERFkZGSQkZFBaWkpAAcPHuS3v/0tmzZt4siRI3z88cfccccdjBs3jiFDhgAwefJkBg4cyO23387WrVv57LPPePzxx5k9e3aNozsiIiLeqKioYNOmTUycONG9zWw2M3HiRNatW1fjMeXl5QQHB1fbFhIScl5qt4iItG5epb29/PLLgFHO+mxvvPEGd911F4GBgXz++ecsXLiQ4uJikpKSmDFjBo8//rh7X4vFwieffMJ9993HmDFjCAsL484776y2LpCIiEhDZWdnY7fbayyus2fPnhqPmTJlCgsWLGDcuHH06tWLlStXsnTpUux2e63XObcSaUFBAWCkedhsNq/b7TqmIcdK01Lf+Df1j/9qrr7x5vxeBT/1LQmUlJTEV199Ve95unfvzrJly7y5tIi0Mna7XX+ImpHNZiMgIICysrI6b9g9YbVasVgsPmpZ6/Diiy8ya9Ys+vfvj8lkolevXsycObPWNDmovRLpihUrqi3ufS6z2VxrKdaAgAC+/PJL71+ANAmHw4HD4XA/Tk1NbcHWSH3UP/6rqfumpKTE430btc6PiEhNioqKOH78eL0fmIjvOJ1O4uPjOXbsWKOLEZhMJrp27Up4eLiPWte8YmJisFgs7mI6LpmZmbUW1uncuTMfffQRZWVl5OTk0KVLF371q1/Rs2fPWq9zbiVSV7WhyZMn11jwwGazkZmZ6U4VP5fT6aSsrIzg4GC/LyjRnoSEhBAdHc2qVauYNGmSJtT7IZvNRmpqqvrHDzVX37hG3j2h4EdEfMput3P8+HFCQ0Pp3LmzbuKaicPhoKioiPDw8EYtPup0OsnKyuL48eP06dOnVY4ABQYGMmLECFauXMn06dMB4+ezcuVK5syZU+exwcHBJCYmYrPZ+OCDD/jRj35U6761VSK1Wq3n/ZF3OBwcOnQIi8VCYmIigYGB5703fNWH4htOp5OKigqysrJIT08Hau5b8R/qH//V1H3jzbkV/IiIT9lsNpxOJ507dyYkJKSlm9NuOBwOKioqCA4ObvSNc+fOnTly5Ag2m61VBj8AKSkp3HnnnYwcOZJRo0a556LOnDkTgDvuuIPExETmz58PwPr16zlx4gTDhg3jxIkTPP300zgcDh599FGftKeiogKHw0FSUlKtKXG+7EPxjZCQEKxWK0eOHGm17wURqU7Bj4g0CY34tF5toe9uuukmsrKyePLJJ8nIyGDYsGEsX77cXQQhLS2tWoBRVlbG448/zqFDhwgPD2fq1Km89dZbREVF+bRdCmpaH1eftYX3hYgo+BERkTZqzpw5taa5rVq1qtrjyy+/nF27djVDq0REpCXpIygRER9LTk5m4cKFjTrHXXfd5Z6vItJW+OK9ISLSGBr5ERHBWL9s2LBhPrkx27hxI2FhYY1vlIgf0HtDRNoSBT8iIh5wOp3Y7XYCAur/tdm5c+dmaJGIf9B7Q0RaE6W9iUi7d9ddd/HVV1/x4osvYjKZMJlMvPnmm5hMJj799FNGjBhBUFAQq1ev5uDBg1x33XXExcURHh7ORRddxOeff17tfOem9phMJl599VWuv/56QkND6dOnDx9//LFXbSwvL+fnP/85sbGxBAcHM3bsWDZu3Oh+/vTp08yaNYu4uDhCQkLo06cPb7zxBmBUGpszZw4JCQkEBwfTvXt3d5Uzkbr443tj48aNTJo0iZiYGDp06MDll1/O5s2bq+2Tl5fHz372M+Li4ggODmbQoEF88skn7ufXrFnD+PHjCQ0NpWPHjkyZMoXTp083/gcmIn6vXQY/lr9fwxW7HoOCky3dFJE2z+l0UlJR2SL/PF1k9cUXX2TMmDHMmjWL9PR00tPTSUpKAuBXv/oVf/jDH9i9ezdDhgyhqKiIqVOnsnLlSr7//nuuuuoqpk2bRlpaWp3XmDdvHj/60Y/Ytm0bU6dO5bbbbiM3N9f9fHJyMk8//XStxz/66KN88MEH/P3vf2fz5s307t2bKVOmuM/x5JNPsnfvXv73v/+xe/duXn75ZWJiYgD405/+xMcff8y//vUv9u7dyzvvvENycrJHPxtpGrW9L0or7H7zvgD/fG8UFhZy5513snr1ar799lv69OnD1KlTKSwsBIyS4VdffTVr1qzh7bffZteuXfzhD39wl6resmULV155JQMHDmTdunWsXr2aadOmYbfbPf65iDSVpZuPc+frG8gqLG/pprRZ7TLtzZSzj4jy09gqilq6KSJtXqnNzsAnP2uRa+96ZgqhgfX/muvQoQOBgYGEhoYSHx8PwJ49ewB45plnmDRpknvf6Ohohg4d6n7829/+lg8//JCPP/64zgU077rrLm655RYAnn32Wf70pz+xYcMGrrrqKgB69erlDlbOVVxczMsvv8ybb77J1VdfDcArr7xCamoqr732Go888ghpaWkMGTKEkSNHYjabqwU3aWlp9OnTh7Fjx2IymejevXu9PxNpWq3hfQH++d644oorqh3/t7/9jaioKL766iuuvfZaPv/8czZs2MDu3bvp27cvAD179nTv/8c//pGRI0fy0ksvubddcMEFHv08RJrS92mnefTf26h0OPnHuiP8cnK/lm5Sm9QuR34IMBZeNFUUt3BDRMTfjRw5strjoqIiHn74YQYMGEBUVBTh4eHs3r273k+3hwwZ4v4+LCyMyMhITp065d62cuXKWm8QDx48iM1m49JLL3Vvs1qtjBo1it27dwNw7733snTpUi688EIeffRR1q5d6973rrvuYsuWLfTr14+f//znrFixwvMfgEgtWuq9kZmZyaxZs+jTpw8dOnQgMjKSoqIi93W2bNlC165d3YHPuVwjPyL+pKDMxgP//J5KhzEyu3TzCRwOz0dpxXPtcuSHwKrVtW0lLdsOkXYgxGph1zNTWuzajXVuZaqHH36Y1NRUnn/+eXr37k1ISAg33ngjFRUVdZ7HarVWe2wymXA4HI1un8vVV1/Ntm3b+Oabb1i5ciVXXnkls2fP5vnnn+fCCy/k8OHDfPrpp3z++ef86Ec/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5FTBj4gY/va3v9GlSxccDke17ddddx0/+clPOHjwINdddx1xcXGEh4dz0UUX8fnnnzf6ui+//DK9evUiMDCQfv368dZbb7mfczqdPP3003Tr1o2goCC6dOnCz3/+c/fzL730En369CE4OJiEhATuvPPORrdH5GzN8b7IycnhlltuITExkdDQUAYPHsw///nPavs4HA7++Mc/0rt3b4KCgujWrRu///3v3c8fP36cW265hejoaMLCwhg5ciTr169v+AuXdsk13ycp2rhP7B8fATSu4tuBrCJKbXbCAi306hze4PMkx4QxontHHE74z5aT9R/gdEL+cTjnvdskbGVwYCV89hv42wRYcAH8Lh6e7YJ10YVcvu9pAt67GT66F1Y8DqtfgO/fgr3L4PgGyD1oBD5gBENNrF2O/LiCH5OtxPhPYW6fMaBIs3A6W+6DBmsomEwe7frDH/6QBx54gC+//JIrr7wSgNzcXJYvX86yZcsoKipi6tSp/P73vycoKIh//OMfTJs2jb1799KtW7caz3nXXXdx5MgRVq1aVePzH374IQ8++CALFy5k4sSJfPLJJ8ycOZOuXbsyYcIEPvjgA1544QXeffddLrjgAjIyMti6dSsA3333HT//+c956623uOSSS8jOzvZJMCbNpKb3hcNhbKuwNO3fJT97X5SVlTFixAgee+wxIiMj+d///sftt99Or169GDVqFABz587llVde4YUXXmDs2LGkp6ezZ88eAIqKirj88stJTEzk448/Jj4+ns2bN58XsInUxzXy060q+BmQEAnArkYUPdhWlfI2KLEDFrNn77vaXD88kU1HT7P0+xPMGtez9h1tZbD0p7D7vxAcBd0uhm5jjH9dhkFAUKPagdMJOQeMgOfA53Bkda0VlJ2WQMrMYQRHd8UUFmOM6oR2gtAYCOt01vdV20OiG9c2D7TP4MdV6hqgsqz6YxHxLVsJPNulZa7965MQWPcK2i4dO3bk6quvZsmSJe6bvH//+9/ExMQwYcIEzGYzQ4cOde//29/+lg8//JCPP/6YOXPm1HjOhISEOm/Ann/+ee666y7uv/9+AFJSUvj22295/vnnmTBhAmlpacTHxzNx4kSsVivdunVz3wympaURFhbGtddeS0REBElJSfTq1cuj1yp+oIb3hRmIao5r+9n7IjExkYcfftj9+IEHHuCzzz7j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jxo1ildffbWwnM1m+eIXv8i+fft44YUXGDduXH83tezMmjWLRx99lMsvv5xTp06xbt06PvnJT/KPf/yDwYMHA4pLVDQ2NgJ0+z7K35bX0tLC5z73OVKpFDt27GDIkCH91s5yNX/+fBYtWsSkSZM4fPgw3/nOd1iwYAG7du3Cdd3CdopNNCgvRZtyU2lQXoq+KOemAV38vF933HEHiUSC3bt3v2v3tnx4FixYUJifOnUqs2bNYsKECfz617/mtttuAxSXUrR06VIuvvhinn/+eZLJZLGbUxa+8IUvFOanTJnC1KlTufTSS6mrq2POnDmF2xSb0qLPv+JQbhp49NlXHFHOTQN62FtNTQ2u63L69OlO60+fPs3o0aMLy9dddx0nTpzgj3/8Y383UXKGDh3KRz/6UQ4dOlRYp7hEQ/698l7vI4DPfvazNDQ0dDvsQPrH5MmTqamp6fReAsUmKpSXSotyUzQpL5WeKOWmAV38xONxpk+fzvbt2wvrgiBg+/btzJ49u7Du+uuv55e//CVf/vKXeeKJJ4rR1LLX0tLC4cOHGTNmTGGd4hINkyZNYvTo0Z3eR01NTezZs6fT+whg5cqV3HfffVx//fX8+c9/7u+mCvDGG2/w9ttvd3ovgWITFcpLpUW5KZqUl0pPpHKTHeCeeOIJm0gk7KOPPmoPHDhgv/KVr9ihQ4faxsZGa621gH3qqaestdY++eSTtqKiwj755JNFbHF5+MY3vmHr6urskSNH7Isvvmhra2ttTU2NffPNN621ikt/a25utvv27bP79u2zgF2/fr3dt2+fff3116211t5333126NCh9plnnrENDQ124cKFdtKkSbatrc1aa+2RI0csYPft22ettfahhx6yVVVVtr6+vliHNGC8W2yam5vtN7/5Tbtr1y575MgR+6c//cleffXV9rLLLrPt7e3WWsUmipSXoku5KTqUl6KtlHPTgC9+rLV248aN9pJLLrHxeNzOnDnT7t69u3DbuR9k1lq7detWW1FRYX/7298WoaXlY8mSJXbMmDE2Ho/bcePG2SVLlthDhw4Vbldc+teOHTss0GW69dZbrbXWBkFgv//979tRo0bZRCJh58yZYw8ePFi4//kfYtZa++CDD9rBgwfbF198sZ+PZmB5t9i0trbauXPn2hEjRthYLGYnTJhgV6xYUTiJtlaxiSrlpWhSbooO5aVoK+XcZKy1tu/6lURERERERKJhQP/Pj4iIiIiISJ6KHxERERERKQsqfkREREREpCyo+BERERERkbKg4kdERERERMqCih8RERERESkLKn5ERERERKQsqPgREREREZGyoOJHRERERETKgoofkT6ybNkybrjhhmI3Q0REpEC5Scqdih8RERERESkLKn5Eeuk3v/kNU6ZMIZlMMnz4cGpra7nzzjt57LHHeOaZZzDGYIyhrq4OgOPHj3PTTTcxdOhQhg0bxsKFCzl69Ghhf/mrcuvWrWPEiBFUV1dz++23k06ni3OAIiJScpSbRLrnFbsBIqXs1KlTLF26lPvvv58bb7yR5uZm6uvrueWWWzh27BhNTU088sgjAAwbNoxMJsO8efOYPXs29fX1eJ7HPffcw/z582loaCAejwOwfft2KioqqKur4+jRoyxfvpzhw4fzox/9qJiHKyIiJUC5SeTCVPyI9MKpU6fIZrMsWrSICRMmADBlyhQAkskkqVSK0aNHF7b/+c9/ThAE/OxnP8MYA8AjjzzC0KFDqaurY+7cuQDE43G2bNlCZWUlV111FT/4wQ+48847+eEPf4jjqMNWREQuTLlJ5ML0ShXphWnTpjFnzhymTJnC4sWLefjhh3nnnXcuuP3LL7/MoUOHGDx4MFVVVVRVVTFs2DDa29s5fPhwp/1WVlYWlmfPnk1LSwvHjx/v0+MREZHSp9wkcmHq+RHpBdd12bZtG3/5y1947rnn2LhxI9/97nfZs2dPt9u3tLQwffp0fvGLX3S5bcSIEX3dXBERKQPKTSIXpuJHpJeMMVx77bVce+21rFmzhgkTJvDUU08Rj8fxfb/TtldffTVbt25l5MiRVFdXX3CfL7/8Mm1tbSSTSQB2795NVVUV48eP79NjERGRgUG5SaR7GvYm0gt79uzhxz/+MX/96185duwYv/vd73jrrbe48sormThxIg0NDRw8eJAzZ86QyWS4+eabqampYeHChdTX13PkyBHq6ur42te+xhtvvFHYbzqd5rbbbuPAgQM8++yzrF27llWrVmlMtYiIvCflJpELU8+PSC9UV1ezc+dONmzYQFNTExMmTODBBx9kwYIFzJgxg7q6OmbMmEFLSws7duzgM5/5DDt37uRb3/oWixYtorm5mXHjxjFnzpxOV9vmzJnDZZddxqc+9SlSqRRLly7l7rvvLt6BiohIyVBuErkwY621xW6EiHRYtmwZ//rXv3j66aeL3RQRERFAuUkGDvVTioiIiIhIWVDxIyIiIiIiZUHD3kREREREpCyo50dERERERMqCih8RERERESkLKn5ERERERKQsqPgREREREZGyoOJHRERERETKgoofEREREREpCyp+RERERESkLKj4ERERERGRsqDiR0REREREysL/A7kBJEh3yoByAAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def plot_record_curves(record_dict, sample_step=500):\n",
    "    # .set_index(\"step\") 将 step 列设置为 DataFrame 的索引\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    last_step = train_df.index[-1]  # 最后一步的步数\n",
    "\n",
    "    # print(train_df)\n",
    "    # print(val_df)\n",
    "\n",
    "    # 画图 \n",
    "    fig_num = len(train_df.columns)  # 画两张图,分别是损失和准确率\n",
    "\n",
    "    # plt.subplots：用于创建一个包含多个子图的图形窗口。\n",
    "    # 1：表示子图的行数为 1。\n",
    "    # fig_num：表示子图的列数，即子图的数量。\n",
    "    # figsize=(5 * fig_num, 5)：设置整个图形窗口的大小，宽度为 5 * fig_num，高度为 5。\n",
    "    # fig：返回的图形对象（Figure），用于操作整个图形窗口。\n",
    "    # axs：返回的子图对象（Axes 或 Axes 数组），用于操作每个子图。\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        # train_df.index 是 x 轴数据（通常是 step）。\n",
    "        # train_df[item] 是 y 轴数据（当前指标的值）。\n",
    "        axs[idx].plot(train_df.index, train_df[item], label=\"train:\" + item)\n",
    "        # val_df.index 是 x 轴数据。\n",
    "        # val_df[item] 是 y 轴数据。\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=\"val:\" + item)\n",
    "        axs[idx].grid()  # 显示网格\n",
    "        axs[idx].legend()  # 显示图例\n",
    "        axs[idx].set_xticks(range(0, train_df.index[-1] + 1, 5000))  # 设置x轴刻度\n",
    "        axs[idx].set_xticklabels(map(lambda x: f\"{x // 1000}k\", range(0, last_step + 1, 5000)))  # 设置x轴标签\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "plot_record_curves(record_dict)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d9729d15a0d01e8",
   "metadata": {},
   "source": [
    "## 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "3195e10a60c29806",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.121498Z",
     "start_time": "2025-01-21T14:10:19.121498Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test loss: 0.4968, Test acc: 0.8334\n"
     ]
    }
   ],
   "source": [
    "# 加载最好的模型\n",
    "# torch.load：加载保存的模型权重或整个模型。\n",
    "# \"checkpoints/best.ckpt\"：模型权重文件路径。\n",
    "# weights_only=True：仅加载模型的权重，而不是整个模型（包括结构和参数）。这是 PyTorch 2.1 引入的新特性，用于增强安全性。\n",
    "# map_location=device：将模型加载到当前设备（GPU或CPU）。\n",
    "model.load_state_dict(torch.load(\"checkpoints/08_resnet.ckpt\", weights_only=True, map_location=device))  # 加载最好的模型\n",
    "\n",
    "model.eval()  # 评估模式\n",
    "loss, acc = evaluate(model, eval_loader, loss_fct)\n",
    "print(f\"Test loss: {loss:.4f}, Test acc: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
